home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Cream of the Crop 1
/
Cream of the Crop 1.iso
/
PROGRAM
/
ZTIMER11.ARJ
/
LZTIMER.ASM
< prev
next >
Wrap
Assembly Source File
|
1992-04-21
|
23KB
|
778 lines
;****************************************************************************
;*
;* Long Period Zen Timer
;*
;* From the book
;* "Zen of Assembly Language"
;* Volume 1, Knowledge
;*
;* by Michael Abrash
;*
;* Modifications by Kendall Bennett
;*
;* Filename: $RCSfile: lztimer.asm $
;* Version: $Revision: 1.4 $
;*
;* Language: 8086 Assembler
;* Environment: IBM PC (MS DOS)
;*
;* Description: Uses the 8253 timer and the BIOS time-of-day count to time
;* the performance of code that takes less than an hour to
;* execute.
;*
;* Because interrupts are left on (in order to allow the timer
;* interrupt to be recognised), this is less accurate than the
;* precision Zen Timer, so it is best used only to time code
;* that takes more than about 54 milliseconds to execute (code
;* that causes the precision Zen Timer to overflow). Resolution
;* is limited by the occurrence of timer interrupts.
;*
;* Externally 'C' callable routines:
;*
;* LZTimerOn: Saves the BIOS time of day count and starts the
;* long period Zen Timer.
;*
;* LZTimerOff: Stops the long-period Zen Timer and saves the timer
;* count and the BIOS time of day count.
;*
;* LZTimerReport: Prints the net time that passed between starting and
;* stopping the timer.
;*
;* LZTimerCount: Returns an unsigned long representing the timed count
;* in microseconds. If more than an hour passed or
;* midnight passed during the timing interval, LZTimerCount
;* will return the value 0xFFFFFFFF (an invalid count).
;*
;* Note: If either more than an hour passes or midnight falls between
;* calls to LZTimerOn and LZTimerOff, an error is reported. For
;* timing code that takes more than a few minutes to execute,
;* either the DOS TIME command in a batch file before and after
;* execution of the code to time or the use of the DOS time-of-day
;* function in place of the long-period Zen Timer is more than
;* adequate.
;*
;* Note: The PS/2 version is assembled by setting the symbol PS2 to 1.
;* PS2 must be set to 1 on PS/2 computers because the PS/2's timers
;* are not compatible with an undocumented timer-stopping feature
;* of the 8253; the alternative timing approach that must be used
;* on PS/2 computers leaves a short window during which the timer 0
;* count and the BIOS timer count may not be synchronised. You
;* should also set the PS2 symbol to 1 if you're getting erratic
;* or obviously incorrect results.
;*
;* Note: When PS2 is 0, the code relies on a undocumented feature of the
;* 8253 to get more reliable readings. It is possible that the
;* 8253 (or whatever chip is emulating the 8253) may be put into
;* an undefined or incorrect state when this feature if used.
;*
;* *****************************************************************
;* * If your computer displays any hint of erratic behaviour *
;* * after the long-period Zen Timer is used, such as the floppy *
;* * drive failing to operate properly, reboot the system, set *
;* * PS2 to 1 and leave it that way! *
;* *****************************************************************
;*
;* Note: Each block of code being timed should ideally be run several
;* times, with at least two similar readings required to
;* establish a true measurement, in order to eliminate any
;* variability caused by interrupts.
;*
;* Note: Interrupts must not be disabled for more than 54 ms at a
;* stretch during the timing interval. Because interrupts are
;* enabled, key, mice, and other devices that generate interrupts
;* should not be used during the timing interval.
;*
;* Note: Any extra code running off the timer interrupt (such as
;* some memory resident utilities) will increase the time
;* measured by the Zen Timer.
;*
;* Note: These routines can introduce inaccuracies of up to a few
;* tenths of a second into the system clock count for each
;* code section being timed. Consequently, it's a good idea to
;* reboot at the conclusion of timing sessions. (The
;* battery-backed clock, if any, is not affected by the Zen
;* timer.)
;*
;* All registers and all flags are preserved by all routines.
;*
;* $Id: lztimer.asm 1.4 92/04/20 17:33:43 kjb release $
;*
;* Revision History:
;* -----------------
;*
;* $Log: lztimer.asm $
;* Revision 1.4 92/04/20 17:33:43 kjb
;* Modified to allow timing across a midnight boundary
;*
;* Revision 1.3 92/01/27 21:39:57 kjb
;* Converted to a memory model independant library, and released to the
;* public.,
;*
;* Revision 1.2 91/11/16 17:11:33 kjb
;* Modified to return a long integer count rather than a string.
;*
;* Revision 1.1 91/11/14 17:16:28 kjb
;* Initial revision
;*
;****************************************************************************
IDEAL
INCLUDE "model.mac" ; Memory model macros
header lztimer ; Set up memory model
;****************************************************************************
;
; Equates used by long period Zen Timer
;
;****************************************************************************
; Set PS2 to 0 to assemble for use on a fully 8253 compatible system. Set
; it to 1 for PS/2 computers and partially compatible 8253 systems.
PS2 equ 1
; Base address of 8253 timer chip
BASE_8253 = 40h
; The address of the timer 0 count registers in the 8253
TIMER_0_8253 = BASE_8253 + 0
; The address of the mode register in the 8253
MODE_8253 = BASE_8253 + 3
; The address of the BIOS timer count variable in the BIOS data area.
TIMER_COUNT = 46Ch
; Macro to emulate a POPF instruction in order to fix the bug in some
; 80286 chips which allows interrupts to occur during a POPF even when
; interrupts are disabled.
macro MPOPF
local p1,p2
jmp short p2
p1: iret ; Jump to pushed address & pop flags
p2: push cs ; construct far return address to
call p1 ; the next instructionz
endm
; Macro to delay briefly to ensure that enough time has elapsed between
; successive I/O accesses so that the device being accessed can respond
; to both accesses even on a very fast PC.
macro DELAY
jmp $+2
jmp $+2
jmp $+2
endm
begcodeseg lztimer ; Start of code segment
StartBIOSCountLow dw ? ; BIOS count low word at the start of
; the timing period
StartBIOSCountHigh dw ? ; BIOS count high word at the start of
; the timing period
EndBIOSCountLow dw ? ; BIOS count low word at the end of
; the timing period
EndBIOSCountHigh dw ? ; BIOS count high word at the end of
; the timing period
EndTimedCount dw ? ; Timer 0 count at the end of
; the timing period
ReferenceCount dw ? ; Number of counts required to execute timer
; overhead code
; String printed to report results.
label OutputStr byte
db 0dh, 0ah, 'Timed count: '
TimedCountStr db 10 dup (?)
db ' microseconds', 0ah, 0dh
db '$'
; Temporary storage for timed count as it's divided down by powers
; of ten when converting from doubleword binary to ASCII
CurrentCountLow dw ?
CurrentCountHigh dw ?
; Powers of ten table use to perform division by 10 when doing doubleword
; conversion from binary to ASCII.
label PowersOfTen word
dd 1
dd 10
dd 100
dd 1000
dd 10000
dd 100000
dd 1000000
dd 10000000
dd 100000000
dd 1000000000
label PowersOfTenEnd word
; String printed to report that the high word of the BISO count changed
; while timing (an hour elapsed or midnight was crossed),
; and so the count is invalid and the test needs to be rerun.
label TurnOverStr byte
db 0dh,0ah
db '****************************************************',0dh,0ah
db '* Either midnight passed or an hour or more passed *',0dh,0ah
db '* while timing was in progress. If the former was *',0dh,0ah
db '* the case, please re-run the test; if the latter *',0dh,0ah
db '* was the case, the test code takes too long to *',0dh,0ah
db '* run to be timed by the long-period Zen Timer. *',0dh,0ah
db '* Suggestions: Use the DOS TIME command, the DOS *',0dh,0ah
db '* time function, or a watch! *',0dh,0ah
db '****************************************************',0dh,0ah
db '$'
;----------------------------------------------------------------------------
; void LZTimerOn(void);
;----------------------------------------------------------------------------
; Starts the Long period Zen timer counting.
;----------------------------------------------------------------------------
procfar _LZTimerOn
; Save the context of the program being timed
push ax
pushf
; Set the timer 0 of the 8253 to mode 2 (divide-by-N), to cause
; linear counting rather than count-by-two counting. Also stops
; timer 0 until the timer count is loaded, except on PS/2 computers.
mov al,00110100b ; mode 2
out MODE_8253,al
; Set the timer count to 0, so we know we won't get another timer
; interrupt right away. Note: this introduces and inaccuracy of up to 54 ms
; in the system clock count each time it is executed.
DELAY
sub al,al
out TIMER_0_8253,al ; lsb
DELAY
out TIMER_0_8253,al ; msb
; In case interrupts are disabled, enable interrupts briefly to allow the
; interrupt generated when switching from mode 3 to mode 2 to be recognised.
; Interrupts must be enabled for at least 210 ns to allow time for that
; interrupt to occur. Here, 10 jumps are used for the delay to ensure that
; the delay time will be more than enough even on a very fast PC.
pushf
sti
rept 10
jmp $+2
endm
MPOPF
; Store the timing start BIOS count.
; (Since the timer count was just set to 0, the BIOS count will stay the
; same for the next 54 ms, so we don't need to disable interrupts in order
; to avoid getting a half-changed count.)
push ds
sub ax,ax
mov ds,ax
mov ax,[ds:TIMER_COUNT+2]
mov [StartBIOSCountHigh],ax
mov ax,[ds:TIMER_COUNT]
mov [StartBIOSCountLow],ax
pop ds
; Set the timer count to 0 again to start the timing interval.
mov al,00110100b ; set up to load initial
out MODE_8253,al ; timer count
DELAY
sub al,al
out TIMER_0_8253,al ; load count lsb
DELAY
out TIMER_0_8253,al ; load count msb
; Restore the context and return.
MPOPF ; keeps interrupts off
pop ax
ret
procend _LZTimerOn
;----------------------------------------------------------------------------
; void LZTimerOff(void);
;----------------------------------------------------------------------------
; Stops the long period Zen timer and saves count.
;----------------------------------------------------------------------------
procfar _LZTimerOff
; Save the context of the program being timed
pushf
push ax
push cx
; In case interrupts are disabled, enable interrupts briefly to allow
; any pending interrupts to be handled. Interrupts must be enabled for at
; leas 210 ns to allow time for that interrupt to occur. Here, 10 jumps
; are used for the delay to ensure that the delay will be more than long
; enough even on a very fast PC.
sti
rept 10
jmp $+2
endm
; Latch the timer count.
if PS2
mov al,00000000b ; latch timer 0
out MODE_8253,al
; This is where a one instruction long window exists on the PS/2. The timer
; count and the BIOS count can lose synchronization; since the timer keeps
; counting after it's latched, it can turn over right after it's latched
; and cause the BIOS count to turn over before interrupts are disabled,
; leaving us with a timer count from before the timer turned over coupled
; with a BIOS count from after the timer turned over. The result is a count
; that's 54 ms too long.
else
; Set timer 0 to mode 2 (divide-by-N), waiting for a 2-byte count load,
; which stops timer 0 until the count is loaded. (Only works on fully
; 8253 compatible chips).
mov al,00110100b ; Mode 2
out MODE_8253,al
DELAY
mov al,00000000b ; Latch timer 0 count
out MODE_8253,al
endif
cli ; Stop the BIOS count
; Read the BIOS count. (Since interrupts are disabled, the BIOS
; count won't change).
push ds
sub ax,ax
mov ds,ax
mov ax,[ds:TIMER_COUNT+2]
mov [EndBIOSCountHigh],ax
mov ax,[ds:TIMER_COUNT]
mov [EndBIOSCountLow],ax
pop ds
; Read out the count we latched earlier.
in al,TIMER_0_8253 ; least significant byte
DELAY
mov ah,al
in al,TIMER_0_8253 ; most significant byte
xchg ah,al
neg ax ; Convert from countdown remaining
; to elapsed count
mov [EndTimedCount],ax
; Restart timer 0, which is still waiting for an initial count
; to be loaded.
ife PS2
DELAY
mov al,00110100b ; mode 2, waiting to load a 2 byte count
out MODE_8253,al
DELAY
sub al,al
out TIMER_0_8253,al ; lsb
DELAY
mov al,ah
out TIMER_0_8253,al ; msb
endif
sti ; Let the BIOS count continue
; Time a zero-length code fragment, to get a reference count for how
; much overhead this routine has. Time it 16 times and average it, for
; accuracy, rounding the result.
mov [ReferenceCount],0
mov cx,16
cli ; interrupts off to allow a precise
; reference count
@@RefLoop:
call ReferenceTimerOn
call ReferenceTimerOff
loop @@RefLoop
sti
add [ReferenceCount],8 ; total + (0.5 * 16)
mov cl,4
shr [ReferenceCount],cl ; (total) / 16 + 0.5
; Restore the context of the program being timed and return to it.
pop cx
pop ax
MPOPF
ret
procend _LZTimerOff
;----------------------------------------------------------------------------
; ReferenceTimerOn
;----------------------------------------------------------------------------
; Called by PZTimerOff to start timer for overhead measurements.
;----------------------------------------------------------------------------
proc ReferenceTimerOn far
; Save the context of the program being timed
push ax
pushf ; Interrupts are already off
; Set the timer 0 of the 8253 to mode 2 (divide-by-N), to cause
; linear counting rather than count-by-two counting.
mov al,00110100b ; set up to load
out MODE_8253,al ; timer count
DELAY
; Set the timer count to 0
sub al,al
out TIMER_0_8253,al ; load count lsb
DELAY
out TIMER_0_8253,al ; load count msb
; Restore the context and return.
MPOPF
pop ax
ret
procend ReferenceTimerOn
;----------------------------------------------------------------------------
; ReferenceTimerOff
;----------------------------------------------------------------------------
; Called by PZTimerOff to stop timer and add result to ReferenceCount
; for overhead measurements.
;----------------------------------------------------------------------------
proc ReferenceTimerOff far
; Save the context of the program being timed
pushf
push ax
push cx
; Match the interrupt window delay in LZTimerOff
sti
rept 10
jmp $+2
endm
; Latch the count and read it.
mov al,00000000b ; latch timer 0
out MODE_8253,al
DELAY
in al,TIMER_0_8253 ; least significant byte
DELAY
mov ah,al
in al,TIMER_0_8253 ; most significant byte
xchg ah,al
neg ax ; Convert from countdown remaining
; to elapsed count
add [ReferenceCount],ax
; Restore the context of the program being timed and return to it.
pop cx
pop ax
MPOPF
ret
procend ReferenceTimerOff
;----------------------------------------------------------------------------
; void LZTimerReport(void);
;----------------------------------------------------------------------------
; Report timing results found.
;----------------------------------------------------------------------------
procfar _LZTimerReport
pushf
push ax
push bx
push cx
push dx
push si
push di
push ds
push cs ; DOS functions require that DS point
pop ds ; to text to be displayed on the screen
if codesize
ASSUME ds:lztimer_TEXT
else
ASSUME ds:_TEXT
endif
; See if a midnight boundary has passed and adjust the finishing BIOS
; count by the number of ticks in 24 hours. We wont be able to detect
; more than 24 hours, but at least we can time across a midnight
; boundary
mov ax,[EndBIOSCountHigh] ; Is end < start?
cmp ax,[StartBIOSCountHigh]
ja @@CheckForHour ; No, check for hour passing
jb @@Adjust ; Yes, adjust ending time
mov ax,[EndBIOSCountLow] ; Maybe, check low order words
cmp ax,[StartBIOSCountLow]
jae @@CheckForHour ; No, check for hour passing
; Adjust the finishing time by adding the number of ticks in 24 hours
; (1573040).
@@Adjust:
add [EndBIOSCountLow],00B0h
adc [EndBIOSCountHigh],18h
; See if more than an hour passed during timing. If so, notify the user.
@@CheckForHour:
mov ax,[StartBIOSCountHigh]
cmp ax,[EndBIOSCountHigh]
jz @@CalcBIOSTime ; Hour count didn't change, so
; everything is fine
inc ax
cmp ax,[EndBIOSCountHigh]
jnz @@TestTooLong ; Two hour boundaries passed, so the
; results are no good
mov ax,[EndBIOSCountLow]
cmp ax,[StartBIOSCountLow]
jb @@CalcBIOSTime ; a single hour boundary passed. That's
; OK, so long as the total time wasn't
; more than an hour.
; Over an hour elapsed passed during timing, which renders
; the results invalid. Notify the user. This misses the case where a
; multiple of 24 hours has passed, but we'll rely on the perspicacity of
; the user to detect that case :-).
@@TestTooLong:
mov ah,9
mov dx,offset TurnOverStr
int 21h
jmp short @@Done
; Convert the BIOS time to microseconds
@@CalcBIOSTime:
mov ax,[EndBIOSCountLow]
sub ax,[StartBIOSCountLow]
mov dx,54925 ; Number of microseconds each
; BIOS count represents.
mul dx
mov bx,ax ; set aside BIOS count in
mov cx,dx ; microseconds
; Convert timer count to microseconds
mov ax,[EndTimedCount]
mov si,8381
mul si
mov si,10000
div si ; * 0.8381 = * 8381 / 10000
; Add the timer and BIOS counts together to get an overall time in
; microseconds.
add bx,ax
adc cx,0
; Subtract the timer overhead and save the result
mov ax,[ReferenceCount]
mov si,8381
mul si
mov si,10000
div si ; * 0.8381 = * 8381 / 10000
sub bx,ax
sbb cx,0
mov [CurrentCountLow],bx
mov [CurrentCountHigh],cx
; Convert the result to an ASCII string by trial subtractions of
; powers of 10.
mov di,(offset PowersOfTenEnd - offset PowersOfTen) - 4
mov si,offset TimedCountStr
@@CTSNextDigit:
mov bl,'0'
@@CTSLoop:
mov ax,[CurrentCountLow]
mov dx,[CurrentCountHigh]
sub ax,[PowersOfTen+di]
sbb dx,[PowersOfTen+di+2]
jc @@CTSNextPowerDown
inc bl
mov [CurrentCountLow],ax
mov [CurrentCountHigh],dx
jmp @@CTSLoop
@@CTSNextPowerDown:
mov [si],bl
inc si
sub di,4
jns @@CTSNextDigit
; Print the results.
mov ah,9
mov dx,offset OutputStr
int 21h
@@Done:
pop ds
pop di
pop si
pop dx
pop cx
pop bx
pop ax
MPOPF
ret
ASSUME ds:DGROUP
procend _LZTimerReport
;----------------------------------------------------------------------------
; unsigned long LZTimerCount(void);
;----------------------------------------------------------------------------
; Returns an unsigned long representing the net time in microseconds.
;
; If either and hour has passed, or midnight passed while timing, we
; return 0xFFFFFFFF as the count (which is not a possible count in itself).
;----------------------------------------------------------------------------
procfar _LZTimerCount
setupDS
; See if a midnight boundary has passed and adjust the finishing BIOS
; count by the number of ticks in 24 hours. We wont be able to detect
; more than 24 hours, but at least we can time across a midnight
; boundary
mov ax,[EndBIOSCountHigh] ; Is end < start?
cmp ax,[StartBIOSCountHigh]
ja @@CheckForHour ; No, check for hour passing
jb @@Adjust ; Yes, adjust ending time
mov ax,[EndBIOSCountLow] ; Maybe, check low order words
cmp ax,[StartBIOSCountLow]
jae @@CheckForHour ; No, check for hour passing
; Adjust the finishing time by adding the number of ticks in 24 hours
; (1573040).
@@Adjust:
add [EndBIOSCountLow],00B0h
adc [EndBIOSCountHigh],18h
; See if more than an hour passed during timing. If so, notify the user.
@@CheckForHour:
mov ax,[StartBIOSCountHigh]
cmp ax,[EndBIOSCountHigh]
jz @@CalcBIOSTime ; Hour count didn't change, so
; everything is fine
inc ax
cmp ax,[EndBIOSCountHigh]
jnz @@TestTooLong ; Two hour boundaries passed, so the
; results are no good
mov ax,[EndBIOSCountLow]
cmp ax,[StartBIOSCountLow]
jb @@CalcBIOSTime ; a single hour boundary passed. That's
; OK, so long as the total time wasn't
; more than an hour.
; Over an hour elapsed passed during timing, which renders
; the results invalid. Notify the user. This misses the case where a
; multiple of 24 hours has passed, but we'll rely on the perspicacity of
; the user to detect that case :-).
@@TestTooLong:
mov ax,0FFFFh
mov dx,0FFFFh
jmp short @@Done
; Convert the BIOS time to microseconds
@@CalcBIOSTime:
mov ax,[EndBIOSCountLow]
sub ax,[StartBIOSCountLow]
mov dx,54925 ; Number of microseconds each
; BIOS count represents.
mul dx
mov bx,ax ; set aside BIOS count in
mov cx,dx ; microseconds
; Convert timer count to microseconds
mov ax,[EndTimedCount]
mov si,8381
mul si
mov si,10000
div si ; * 0.8381 = * 8381 / 10000
; Add the timer and BIOS counts together to get an overall time in
; microseconds.
add bx,ax
adc cx,0
; Subtract the timer overhead and save the result
mov ax,[ReferenceCount]
mov si,8381
mul si
mov si,10000
div si ; * 0.8381 = * 8381 / 10000
sub bx,ax
sbb cx,0
mov ax,bx
mov dx,cx
@@Done:
restoreDS
ret
procend _LZTimerCount
endcodeseg lztimer
END ; End of module